//
// Created by Administrator on 2021/11/23.
//

#include "Application.h"
#include "Camera.h"
#include "Geometry.hpp"
#include "ShaderProgram.h"
#include "MeshUtility.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <vector>

class App : public Application
{
    struct PerDraw
    {
        Mat4x4f viewMatrix;
        Mat4x4f projMatrix;
        Vec3f cameraPos;
    };

    struct PerObject
    {
        Mat4x4f modelMatrix[2];
    };

public:
    App() : Application("Cubemap", 800, 600) {}

protected:
    void OnCreate() override
    {
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glEnable(GL_DEPTH_TEST);

        // 天空盒是一个[1, 1]^3立方体，因此如果近裁剪平面离摄像机太远，那么很可能导致立方体有一部分会在落在
        // 近裁剪平面外面，z比近裁剪平面要小
        m_camera.Setup(800.0f / 600.0f, 60.0f, 0.1f, 100.0f);
        m_camera.transform().position() = Vec3f(0.0f, 0.0f, 10.0f);

        glCreateVertexArrays(static_cast<int>(VAO::Count), m_vao);
        glCreateBuffers(static_cast<int>(BO::Count), m_bo);

        SetupUBO();
        SetupSkybox();
        SetupCube();
        SetupSphere();
    }

    void OnUpdate() override
    {
        UpdatePerDrawUBO();
        UpdatePerObjectUBO();
    }

    void OnRender() override
    {
        glClearColor(1.0f, 0.1f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        RenderCube();
        RenderSphere();
        RenderSkybox();
    }

    void OnDestroy() override
    {
        glDeleteVertexArrays(static_cast<int>(VAO::Count), m_vao);
        glDeleteBuffers(static_cast<int>(BO::Count), m_bo);
        glDeleteTextures(static_cast<int>(Tex::Count), m_tex);
    }

    void OnKey(int keyCode, int action, int mods) override
    {
        switch (keyCode)
        {
            case GLFW_KEY_A:
                m_camera.transform().rotation().y() += 0.1f;
                break;
            case GLFW_KEY_W:
                m_camera.transform().position().z() -= 0.1f;
                break;
            case GLFW_KEY_D:
                m_camera.transform().rotation().y() -= 0.1f;
                break;
            case GLFW_KEY_S:
                m_camera.transform().position().z() += 0.1f;
                break;
        }
    }

private:
    static void LoadCubemap(const std::vector<std::string>& paths, GLuint& tex)
    {
        glCreateTextures(GL_TEXTURE_CUBE_MAP, 1, &tex);
        glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
        int width, height, channel;
        for(int i = 0; i < paths.size(); ++i)
        {
            stbi_uc* data = stbi_load(paths[i].c_str(), &width, &height, &channel, 0);
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
            stbi_image_free(data);
        }
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }

    void SetupUBO()
    {
        glBindBuffer(GL_UNIFORM_BUFFER, m_bo[static_cast<int>(BO::PerDrawUBO)]);
        glBufferData(GL_UNIFORM_BUFFER, sizeof(PerDraw), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 0, m_bo[static_cast<int>(BO::PerDrawUBO)]);
        glBindBuffer(GL_UNIFORM_BUFFER, m_bo[static_cast<int>(BO::PerObjectUBO)]);
        glBufferData(GL_UNIFORM_BUFFER, sizeof(PerObject), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 1, m_bo[static_cast<int>(BO::PerObjectUBO)]);
    }

    void SetupSkybox()
    {
        Vertex* skyboxVertices;
        GLuint* skyboxIndices;
        int skyboxVertexCount;
        MeshUtility::GenerateSkybox(skyboxVertices, skyboxIndices, skyboxVertexCount, m_indexCount[static_cast<int>(VAO::Skybox)]);
        m_sp[static_cast<int>(Shader::Skybox)] = ShaderProgram("../../Cubemap/shaders/skybox_vs.glsl",
                                                               "../../Cubemap/shaders/skybox_fs.glsl");

        std::vector<std::string> skyboxTex = {
                "../../Cubemap/textures/right.jpg",
                "../../Cubemap/textures/left.jpg",
                "../../Cubemap/textures/top.jpg",
                "../../Cubemap/textures/bottom.jpg",
                "../../Cubemap/textures/front.jpg",
                "../../Cubemap/textures/back.jpg",
        };
        LoadCubemap(skyboxTex, m_tex[static_cast<int>(Tex::Skybox)]);

        glBindVertexArray(m_vao[static_cast<int>(VAO::Skybox)]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_bo[static_cast<int>(BO::SkyboxEBO)]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount[static_cast<int>(VAO::Skybox)] * sizeof(GLuint), skyboxIndices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, m_bo[static_cast<int>(BO::SkyboxVBO)]);
        glBufferData(GL_ARRAY_BUFFER, skyboxVertexCount * sizeof(Vertex), skyboxVertices, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        glEnableVertexAttribArray(0);

        delete skyboxVertices;
        delete skyboxIndices;
    }

    void SetupCube()
    {
        Vertex* vertices;
        GLuint* indices;
        int vertexCount;
        MeshUtility::GenerateCube(1.0f, vertices, indices, vertexCount, m_indexCount[static_cast<int>(VAO::Cube)]);

        m_sp[static_cast<int>(Shader::Cube)] = ShaderProgram("../../Cubemap/shaders/cube_vs.glsl",
                                                             "../../Cubemap/shaders/cube_fs.glsl");

        glCreateTextures(GL_TEXTURE_2D, 1, &m_tex[static_cast<int>(Tex::Cube)]);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, m_tex[static_cast<int>(Tex::Cube)]);
        stbi_set_flip_vertically_on_load(true);
        int width, height, channel;
        auto* data = stbi_load("../../Cubemap/textures/cube.png", &width, &height, &channel, 0);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        delete data;

        glBindVertexArray(m_vao[static_cast<int>(VAO::Cube)]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_bo[static_cast<int>(BO::CubeEBO)]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount[static_cast<int>(VAO::Cube)] * sizeof(GLuint), indices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, m_bo[static_cast<int>(BO::CubeVBO)]);
        glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertices, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(Vec3f)));
        glEnableVertexAttribArray(2);

        delete vertices;
        delete indices;
    }

    void SetupSphere()
    {
        Vertex* vertices;
        GLuint* indices;
        int vertexCount;
        MeshUtility::GenerateSphere(40, 1.0f, vertices, indices, vertexCount, m_indexCount[static_cast<int>(VAO::Sphere)]);

        m_sp[static_cast<int>(Shader::Sphere)] = ShaderProgram("../../Cubemap/shaders/sphere_vs.glsl",
                                                             "../../Cubemap/shaders/sphere_fs.glsl");

        glCreateTextures(GL_TEXTURE_2D, 1, &m_tex[static_cast<int>(Tex::Sphere)]);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, m_tex[static_cast<int>(Tex::Sphere)]);
        stbi_set_flip_vertically_on_load(true);
        int width, height, channel;
        auto* data = stbi_load("../../Cubemap/textures/sphere.png", &width, &height, &channel, 0);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        delete data;

        glBindVertexArray(m_vao[static_cast<int>(VAO::Sphere)]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_bo[static_cast<int>(BO::SphereEBO)]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount[static_cast<int>(VAO::Sphere)] * sizeof(GLuint), indices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, m_bo[static_cast<int>(BO::SphereVBO)]);
        glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertices, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(Vec3f)));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(Vec3f)));
        glEnableVertexAttribArray(2);

        delete vertices;
        delete indices;
    }

    void UpdatePerDrawUBO()
    {
        auto* ptr = static_cast<PerDraw*>(glMapNamedBuffer(m_bo[static_cast<int>(BO::PerDrawUBO)], GL_WRITE_ONLY));
        ptr->viewMatrix = m_camera.worldToCameraMatrix().Transposed();
        ptr->projMatrix = m_camera.projectionMatrix().Transposed();
        ptr->cameraPos = m_camera.transform().position();
        glUnmapNamedBuffer(m_bo[static_cast<int>(BO::PerDrawUBO)]);
    }

    void UpdatePerObjectUBO()
    {
        auto* ptr = static_cast<PerObject*>(glMapNamedBuffer(m_bo[static_cast<int>(BO::PerObjectUBO)], GL_WRITE_ONLY));
        ptr->modelMatrix[0] = Mat4x4f(2.0f, 0.0f, 0.0f, -2.0f,
                                   0.0f, 2.0f, 0.0f, 0.0f,
                                   0.0f, 0.0f, 2.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f).Transposed();
        ptr->modelMatrix[1] = Mat4x4f(2.0f, 0.0f, 0.0f, 2.0f,
                                      0.0f, 2.0f, 0.0f, 0.0f,
                                      0.0f, 0.0f, 2.0f, 0.0f,
                                      0.0f, 0.0f, 0.0f, 1.0f).Transposed();
        glUnmapNamedBuffer(m_bo[static_cast<int>(BO::PerObjectUBO)]);
    }

    void RenderSkybox()
    {
        glDepthFunc(GL_LEQUAL);
        m_sp[static_cast<int>(Shader::Skybox)].Use();
        glBindTexture(GL_TEXTURE_CUBE_MAP, m_tex[static_cast<int>(Tex::Skybox)]);
        glBindVertexArray(m_vao[static_cast<int>(VAO::Skybox)]);
        glDrawElements(GL_TRIANGLES, m_indexCount[static_cast<int>(VAO::Skybox)], GL_UNSIGNED_INT, nullptr);
    }

    void RenderCube()
    {
        glDepthFunc(GL_LESS);
        m_sp[static_cast<int>(Shader::Cube)].Use();
        glBindTexture(GL_TEXTURE_2D, m_tex[static_cast<int>(Tex::Cube)]);
        glBindVertexArray(m_vao[static_cast<int>(VAO::Cube)]);
        glDrawElements(GL_TRIANGLES, m_indexCount[static_cast<int>(VAO::Cube)], GL_UNSIGNED_INT, nullptr);
    }

    void RenderSphere()
    {
        glDepthFunc(GL_LESS);
        m_sp[static_cast<int>(Shader::Sphere)].Use();
        glBindTexture(GL_TEXTURE_2D, m_tex[static_cast<int>(Tex::Sphere)]);
        glBindTexture(GL_TEXTURE_CUBE_MAP, m_tex[static_cast<int>(Tex::Skybox)]);
        glBindVertexArray(m_vao[static_cast<int>(VAO::Sphere)]);
        glDrawElements(GL_TRIANGLES, m_indexCount[static_cast<int>(VAO::Sphere)], GL_UNSIGNED_INT, nullptr);
    }

    enum class VAO
    {
        Skybox,
        Cube,
        Sphere,
        Count
    };
    enum class BO
    {
        SkyboxVBO,
        SkyboxEBO,
        CubeVBO,
        CubeEBO,
        SphereVBO,
        SphereEBO,
        PerDrawUBO,
        PerObjectUBO,
        Count
    };
    enum class Tex
    {
        Cube,
        Skybox,
        Sphere,
        Count
    };
    enum class Shader
    {
        Cube,
        Skybox,
        Sphere,
        Count
    };
    GLuint m_vao[static_cast<int>(VAO::Count)];
    GLuint m_bo[static_cast<int>(BO::Count)];
    GLuint m_tex[static_cast<int>(Tex::Count)];
    ShaderProgram m_sp[static_cast<int>(Shader::Count)];
    int m_indexCount[static_cast<int>(VAO::Count)];

    Camera m_camera;
};

int main()
{
    App().Run();
    return 0;
}